1 module hip.graphics.g2d.animation; 2 3 import hip.util.reflection : ExportD; 4 import hip.error.handler; 5 import hip.assets.textureatlas; 6 7 import hip.api.graphics.color; 8 import hip.api.renderer.texture : IHipTextureRegion; 9 public import hip.api.graphics.g2d.animation; 10 11 /** 12 * This class uses multiplication for selecting the current frame, so, depending on the frame rate, it can cause 13 * frame skipping, for giving better freedom for speeding up animation 14 */ 15 @ExportD class HipAnimationTrack : IHipAnimationTrack 16 { 17 private immutable string _name; 18 19 protected HipAnimationFrame[] frames; 20 protected float accumulator = 0; 21 22 ///Internal state management 23 protected uint framesPerSecond = 0; 24 protected uint currentFrame = 0; 25 26 ///Micro optimization for not doing frames.length - 1 every time 27 private uint lastFrame = 0; 28 29 //Those three are a question if they should be in the track or in the animation controller 30 protected bool isPlaying = false; 31 protected bool isAdvancingForward = true; 32 protected HipAnimationLoopingMode _loopingMode; 33 protected bool _reverse = false; 34 35 this(string trackName, uint framesPerSecond, HipAnimationLoopingMode loopingMode = HipAnimationLoopingMode.none) 36 { 37 this._name = trackName; 38 setFramesPerSecond(framesPerSecond); 39 _loopingMode = loopingMode; 40 } 41 string name() const => _name; 42 HipAnimationLoopingMode loopingMode() const => _loopingMode; 43 HipAnimationLoopingMode loopingMode(HipAnimationLoopingMode loopingMode = HipAnimationLoopingMode.reset) => _loopingMode = loopingMode; 44 bool reverse() const => _reverse; 45 bool reverse(bool setReverse) => _reverse = setReverse; 46 float getDuration() const => cast(float)frames.length / framesPerSecond; 47 48 /** 49 * Use this version if you wish a more custom frame 50 */ 51 IHipAnimationTrack addFrames(HipAnimationFrame[] frame...) 52 { 53 foreach(f; frame) 54 frames~= f; 55 if(frames.length > 0) 56 lastFrame = cast(typeof(lastFrame))frames.length - 1; 57 return this; 58 } 59 60 IHipAnimationTrack addFrames(IHipTextureRegion[] regions...) 61 { 62 foreach(r; regions) 63 frames~= HipAnimationFrame(r); 64 if(frames.length > 0) 65 lastFrame = cast(typeof(lastFrame))frames.length - 1; 66 return this; 67 } 68 void reset(){currentFrame = 0;accumulator = 0;} 69 void setFrame(uint frame) 70 { 71 version(HipOptimize){} 72 else 73 ErrorHandler.assertLazyExit(frame < frames.length, "Frame is out of bounds on track "~name); 74 accumulator = frame*(1.0f/framesPerSecond); 75 currentFrame = frame; 76 } 77 void setFramesPerSecond(uint fps){framesPerSecond = fps;} 78 79 HipAnimationFrame* getFrameForTime(float time) 80 { 81 uint frame = (cast(uint)time*framesPerSecond); 82 if(frame > lastFrame) 83 frame = lastFrame; 84 if(_reverse) 85 frame = lastFrame - frame; 86 return &frames[frame]; 87 } 88 89 HipAnimationFrame* getFrameForProgress(float progress) 90 { 91 uint frame = cast(uint)(progress*frames.length); 92 if(frame > lastFrame) 93 frame = lastFrame; 94 if(_reverse) 95 frame = lastFrame - frame; 96 return &frames[frame]; 97 } 98 99 HipAnimationFrame* update(float dt) 100 { 101 if(frames.length == 0) 102 return null; 103 accumulator+= dt; 104 uint frame = cast(uint)(accumulator*framesPerSecond); 105 if(frame > lastFrame) 106 { 107 final switch(_loopingMode) with(HipAnimationLoopingMode) 108 { 109 case reset: 110 accumulator = 0; 111 frame = 0; 112 break; 113 case pingpong: 114 frame = 0; 115 accumulator = 0; 116 isAdvancingForward = !isAdvancingForward; 117 break; 118 case none: 119 accumulator-=dt; 120 frame = lastFrame; 121 break; 122 } 123 } 124 if((_loopingMode == HipAnimationLoopingMode.pingpong && !isAdvancingForward) || 125 (_reverse && _loopingMode != HipAnimationLoopingMode.pingpong)) 126 frame = lastFrame - frame; 127 currentFrame = frame; 128 return &frames[frame]; 129 } 130 131 132 } 133 134 /** 135 * Currently used as a wrapper for holding animation tracks. Could probably do 136 * advanced work as setting track markers for playing tracks sequentially. Setting general animation 137 * speed 138 */ 139 @ExportD class HipAnimation : IHipAnimation 140 { 141 protected IHipAnimationTrack[string] tracks; 142 immutable string name; 143 protected float timeScale; 144 protected IHipAnimationTrack currentTrack; 145 protected HipAnimationFrame* currentFrame; 146 147 148 this(string name) 149 { 150 this.name = name; 151 this.timeScale = 1.0f; 152 } 153 154 static HipAnimation fromAtlas(HipTextureAtlas atlas, string which, uint fps, HipAnimationLoopingMode loopingMode = HipAnimationLoopingMode.none) 155 { 156 import hip.util.conv:to; 157 HipAnimation ret = new HipAnimation(which); 158 HipAnimationTrack track = new HipAnimationTrack(which, fps, loopingMode); 159 AtlasFrame* frame; 160 int i = 1; 161 while((frame = (which~"_"~to!string(i) in atlas)) != null) 162 { 163 track.addFrames(HipAnimationFrame(frame.region)); 164 i++; 165 } 166 ret.addTrack(track); 167 168 return ret; 169 } 170 171 IHipAnimation addTrack(IHipAnimationTrack track) 172 { 173 if(currentTrack is null) 174 { 175 currentTrack = track; 176 update(0);//Updates the current frame 177 } 178 ErrorHandler.assertExit((track.name in tracks) == null, 179 "Track named "~track.name~" is already on animation '"~name~"'"); 180 tracks[track.name] = track; 181 return this; 182 } 183 IHipAnimationTrack getCurrentTrack() {return currentTrack;} 184 HipAnimationFrame* getCurrentFrame() {return currentFrame;} 185 void setTimeScale(float scale){timeScale = scale;} 186 void play(string trackName) 187 { 188 IHipAnimationTrack* track = trackName in tracks; 189 version(HipOptimize){} 190 else 191 ErrorHandler.assertLazyExit(track != null, 192 "Track "~trackName~" does not exists in the animation '"~name~"'."); 193 194 if(currentTrack !is null) 195 currentTrack.reset(); 196 currentTrack = *track; 197 update(0); //Updates the current frame 198 } 199 200 IHipAnimationTrack getTrack(string trackName) 201 { 202 IHipAnimationTrack* track = trackName in tracks; 203 if(track is null) return null; 204 return *track; 205 } 206 207 208 void update(float dt) 209 { 210 if(currentTrack is null) 211 return; 212 currentFrame = currentTrack.update(dt*timeScale); 213 } 214 }